{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a02867dd",
   "metadata": {},
   "source": [
    "# LLM-as-Judge\n",
    "\n",
    "LangSmith 에서 제공되는 Off-the-shelf Evaluators 를 활용해 보겠습니다.\n",
    "\n",
    "Off-the-shelf Evaluators 는 사전에 정의된 프롬프트 기반의 LLM 평가자를 의미합니다.\n",
    "\n",
    "쉽게 사용할 수 있는 이점이 있지만, 더 확장된 기능을 사용하기 위해서는 직접 평가자를 정의해야 합니다.\n",
    "\n",
    "기본적으로 다음의 3가지 정보를 LLM Evaluator 에 전달하여 평가를 진행합니다.\n",
    "\n",
    "- `input`: 질문. 보통 데이터셋의 Question 이 사용됩니다.\n",
    "- `prediction`: LLM 이 생성한 답변. 보통 모델의 답변이 사용됩니다.\n",
    "- `reference`: 정답 답변, Context 등 변칙적으로 활용이 가능.\n",
    "\n",
    "**참고**\n",
    "- https://docs.smith.langchain.com/evaluation/faq/evaluator-implementations"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "34a30a84",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 설치\n",
    "# !pip install -qU langsmith langchain-teddynote"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d75d1492",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API KEY를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API KEY 정보로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "633d9db2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH16-Evaluations\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a09c8411",
   "metadata": {},
   "source": [
    "## RAG 성능 테스트를 위한 함수 정의"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "011f17eb",
   "metadata": {},
   "source": [
    "테스트에 활용할 RAG 시스템을 생성하겠습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8a79916b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from myrag import PDFRAG\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# PDFRAG 객체 생성\n",
    "rag = PDFRAG(\n",
    "    \"data/SPRI_AI_Brief_2023년12월호_F.pdf\",\n",
    "    ChatOpenAI(model=\"gpt-4o-mini\", temperature=0),\n",
    ")\n",
    "\n",
    "# 검색기(retriever) 생성\n",
    "retriever = rag.create_retriever()\n",
    "\n",
    "# 체인(chain) 생성\n",
    "chain = rag.create_chain(retriever)\n",
    "\n",
    "# 질문에 대한 답변 생성\n",
    "chain.invoke(\"삼성전자가 자체 개발한 생성형 AI의 이름은 무엇인가요?\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75b71e6e",
   "metadata": {},
   "source": [
    "`ask_question` 이라는 이름으로 함수를 생성합니다. 입력으로는 `inputs` 라는 딕셔너리를 받고, 출력으로는 `answer` 라는 딕셔너리를 반환합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a922c6d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질문에 대한 답변하는 함수를 생성\n",
    "def ask_question(inputs: dict):\n",
    "    return {\"answer\": chain.invoke(inputs[\"question\"])}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39bcf1e1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 사용자 질문 예시\n",
    "llm_answer = ask_question(\n",
    "    {\"question\": \"삼성전자가 자체 개발한 생성형 AI의 이름은 무엇인가요?\"}\n",
    ")\n",
    "llm_answer"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "51ad180e",
   "metadata": {},
   "source": [
    "evaluator prompt 출력을 위한 함수를 정의합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c8799bc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# evaluator prompt 출력을 위한 함수\n",
    "def print_evaluator_prompt(evaluator):\n",
    "    return evaluator.evaluator.prompt.pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b281e209",
   "metadata": {},
   "source": [
    "## Question-Answer Evaluator\n",
    "\n",
    "가장 기본 기능을 가진 Evaluator 입니다. 질문(Query) 와 답변(Answer) 을 평가합니다.\n",
    "\n",
    "사용자 입력은 `input` 으로 LLM 이 생성한 답변은 `prediction` 으로 정답 답변은 `reference` 로 정의됩니다.\n",
    "\n",
    "(하지만, Prompt 변수는 `query`, `result`, `answer` 로 정의됩니다.)\n",
    "\n",
    "- `query`: 질문\n",
    "- `result`: LLM 답변\n",
    "- `answer`: 정답 답변"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "61eaae63",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langsmith.evaluation import evaluate, LangChainStringEvaluator\n",
    "\n",
    "# qa 평가자 생성\n",
    "qa_evalulator = LangChainStringEvaluator(\"qa\")\n",
    "\n",
    "# 프롬프트 출력\n",
    "print_evaluator_prompt(qa_evalulator)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7ea33ce",
   "metadata": {},
   "source": [
    "평가를 진행하고, 출력된 URL 로 이동하여 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "07be19cf",
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset_name = \"RAG_EVAL_DATASET\"\n",
    "\n",
    "# 평가 실행\n",
    "experiment_results = evaluate(\n",
    "    ask_question,\n",
    "    data=dataset_name,\n",
    "    evaluators=[qa_evalulator],\n",
    "    experiment_prefix=\"RAG_EVAL\",\n",
    "    # 실험 메타데이터 지정\n",
    "    metadata={\n",
    "        \"variant\": \"QA Evaluator 를 활용한 평가\",\n",
    "    },\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ab107895",
   "metadata": {},
   "source": [
    "![](./assets/output-01.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc7b726d",
   "metadata": {},
   "source": [
    "## Context 에 기반한 답변 Evaluator\n",
    "\n",
    "- `LangChainStringEvaluator(\"context_qa\")`: LLM 체인에 정확성을 판단하는 데 참조 \"context\" 를 사용하도록 지시합니다.\n",
    "- `LangChainStringEvaluator(\"cot_qa\")`: `\"cot_qa\"` 는 `\"context_qa\"` 평가자와 유사하지만, 최종 판결을 결정하기 전에 LLM 의 '추론'을 사용하도록 지시한다는 점에서 차이가 있습니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4261642a",
   "metadata": {},
   "source": [
    "**참고**\n",
    "\n",
    "먼저, Context 를 반환하는 함수를 정의해야 합니다: `context_answer_rag_answer`\n",
    "\n",
    "그 다음, `LangChainStringEvaluator` 를 생성합니다. 생성시 `prepare_data` 를 통해 위에서 정의한 함수의 반환 값을 적절하게 매핑합니다.\n",
    "\n",
    "**세부사항**\n",
    "\n",
    "- `run`: LLM 이 생성한 결과 (`context`, `answer`, `input`)\n",
    "- `example`: 데이터셋에 정의된 데이터입니다. (`question` 과 `answer`)\n",
    "\n",
    "`LangChainStringEvaluator` 이 평가를 수행하기 위하여 다음의 3가지 정보가 필요합니다.\n",
    "\n",
    "- `prediction`: LLM 이 생성한 답변\n",
    "- `reference`: 데이터셋에 정의된 답변\n",
    "- `input`: 데이터셋에 정의된 질문\n",
    "\n",
    "하지만, `LangChainStringEvaluator(\"context_qa\")` 는 `reference` 를 Context 로 사용하기 때문에 다음과 같이 정의합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2db10c25",
   "metadata": {},
   "source": [
    "(참고) 아래는 `context_qa` 평가자를 활용하기 위하여 `context`, `answer`, `question` 을 반환하는 함수를 정의하였습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "53b7f804",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Context 를 반환하는 RAG 결과 반환 함수\n",
    "def context_answer_rag_answer(inputs: dict):\n",
    "    context = retriever.invoke(inputs[\"question\"])\n",
    "    return {\n",
    "        \"context\": \"\\n\".join([doc.page_content for doc in context]),\n",
    "        \"answer\": chain.invoke(inputs[\"question\"]),\n",
    "        \"query\": inputs[\"question\"],\n",
    "    }"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0cb38653",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 함수 실행\n",
    "context_answer_rag_answer(\n",
    "    {\"question\": \"삼성전자가 자체 개발한 생성형 AI의 이름은 무엇인가요?\"}\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c08dbf22",
   "metadata": {},
   "outputs": [],
   "source": [
    "# cot_qa 평가자 생성\n",
    "cot_qa_evaluator = LangChainStringEvaluator(\n",
    "    \"cot_qa\",\n",
    "    prepare_data=lambda run, example: {\n",
    "        \"prediction\": run.outputs[\"answer\"],  # LLM 이 생성한 답변\n",
    "        \"reference\": run.outputs[\"context\"],  # Context\n",
    "        \"input\": example.inputs[\"question\"],  # 데이터셋의 질문\n",
    "    },\n",
    ")\n",
    "\n",
    "# context_qa 평가자 생성\n",
    "context_qa_evaluator = LangChainStringEvaluator(\n",
    "    \"context_qa\",\n",
    "    prepare_data=lambda run, example: {\n",
    "        \"prediction\": run.outputs[\"answer\"],  # LLM 이 생성한 답변\n",
    "        \"reference\": run.outputs[\"context\"],  # Context\n",
    "        \"input\": example.inputs[\"question\"],  # 데이터셋의 질문\n",
    "    },\n",
    ")\n",
    "\n",
    "# evaluator prompt 출력\n",
    "print_evaluator_prompt(context_qa_evaluator)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ccd6f08c",
   "metadata": {},
   "source": [
    "평가를 진행하고, 출력된 URL 로 이동하여 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6457fd86",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 데이터셋 이름 설정\n",
    "dataset_name = \"RAG_EVAL_DATASET\"\n",
    "\n",
    "# 평가 실행\n",
    "evaluate(\n",
    "    context_answer_rag_answer,\n",
    "    data=dataset_name,\n",
    "    evaluators=[cot_qa_evaluator, context_qa_evaluator],\n",
    "    experiment_prefix=\"RAG_EVAL\",\n",
    "    metadata={\n",
    "        \"variant\": \"COT_QA & Context_QA Evaluator 를 활용한 평가\",\n",
    "    },\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "59560d72",
   "metadata": {},
   "source": [
    "![](./assets/output-02.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "73a56015",
   "metadata": {},
   "source": [
    "평가 결과 `Ground Truth` 와 맞지 않은 답변을 생성해도 주어진 `Context` 가 맞다면 **CORRECT** 로 평가됩니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "857bb208",
   "metadata": {},
   "source": [
    "## Criteria\n",
    "\n",
    "기준값 참조 레이블(정답 답변)이 없거나 얻기 힘든 경우 `\"criteria\"` 또는 `\"score\"` 평가자를 사용하여 사용자 지정 기준 집합에 대해 실행을 평가할 수 있습니다. \n",
    "\n",
    "이는 모델의 답변에 대한 **높은 수준의 의미론적 측면을 모니터링** 하려는 경우에 유용합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "af4fc686",
   "metadata": {},
   "source": [
    "LangChainStringEvaluator(\"criteria\", config={ \"criteria\": `아래 중 하나의 criterion` })\n",
    "\n",
    "| 기준 | 설명 |\n",
    "|------|------|\n",
    "| `conciseness` | 답변이 간결하고 간단한지 평가 |\n",
    "| `relevance` | 답변이 질문과 관련이 있는지 평가 |\n",
    "| `correctness` | 답변이 옳은지 평가 |\n",
    "| `coherence` | 답변이 일관성이 있는지 평가 |\n",
    "| `harmfulness` | 답변이 해롭거나 유해한지 평가 |\n",
    "| `maliciousness` | 답변이 악의적이거나 악화시키는지 평가 |\n",
    "| `helpfulness` | 답변이 도움이 되는지 평가 |\n",
    "| `controversiality` | 답변이 논란이 되는지 평가 |\n",
    "| `misogyny` | 답변이 여성을 비하하는지 평가 |\n",
    "| `criminality` | 답변이 범죄를 촉진하는지 평가 |\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6d0b15f9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langsmith.evaluation import evaluate, LangChainStringEvaluator\n",
    "\n",
    "# 평가자 설정\n",
    "criteria_evaluator = [\n",
    "    LangChainStringEvaluator(\"criteria\", config={\"criteria\": \"conciseness\"}),\n",
    "    LangChainStringEvaluator(\"criteria\", config={\"criteria\": \"misogyny\"}),\n",
    "    LangChainStringEvaluator(\"criteria\", config={\"criteria\": \"criminality\"}),\n",
    "]\n",
    "\n",
    "# 데이터셋 이름 설정\n",
    "dataset_name = \"RAG_EVAL_DATASET\"\n",
    "\n",
    "# 평가 실행\n",
    "experiment_results = evaluate(\n",
    "    ask_question,\n",
    "    data=dataset_name,\n",
    "    evaluators=criteria_evaluator,\n",
    "    experiment_prefix=\"CRITERIA-EVAL\",\n",
    "    # 실험 메타데이터 지정\n",
    "    metadata={\n",
    "        \"variant\": \"criteria 를 활용한 평가\",\n",
    "    },\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b4a34bbf",
   "metadata": {},
   "source": [
    "![](./assets/output-03.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f562976",
   "metadata": {},
   "source": [
    "## 정답이 존재하는 경우 Evaluator 활용(labeled_criteria)\n",
    "\n",
    "정답이 존재하는 경우, LLM 이 생성한 답변과 정답 답변을 비교하여 평가가 가능합니다.\n",
    "\n",
    "아래의 예시처럼 `reference` 에는 정답 답변을, `prediction` 에는 LLM 이 생성한 답변을 전달합니다.\n",
    "\n",
    "이 처럼 별도의 설정은 `prepare_data` 를 통해 정의합니다.\n",
    "\n",
    "또한, 답변 평가에 활용되는 LLM 은 `config` 의 `llm` 을 통해 정의합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d2abc8f3",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langsmith.evaluation import LangChainStringEvaluator\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# labeled_criteria 평가자 생성\n",
    "labeled_criteria_evaluator = LangChainStringEvaluator(\n",
    "    \"labeled_criteria\",\n",
    "    config={\n",
    "        \"criteria\": {\n",
    "            \"helpfulness\": (\n",
    "                \"Is this submission helpful to the user,\"\n",
    "                \" taking into account the correct reference answer?\"\n",
    "            )\n",
    "        },\n",
    "        \"llm\": ChatOpenAI(temperature=0.0, model=\"gpt-4o-mini\"),\n",
    "    },\n",
    "    prepare_data=lambda run, example: {\n",
    "        \"prediction\": run.outputs[\"answer\"],\n",
    "        \"reference\": example.outputs[\"answer\"],  # 정답 답변\n",
    "        \"input\": example.inputs[\"question\"],\n",
    "    },\n",
    ")\n",
    "\n",
    "# evaluator prompt 출력\n",
    "print_evaluator_prompt(labeled_criteria_evaluator)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6bde4831",
   "metadata": {},
   "source": [
    "아래는 `relevance` 를 평가하는 예시입니다.\n",
    "\n",
    "이번에는 `prepare_data` 를 통해 `reference` 를 `context` 로 전달합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cfa4de48",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "relevance_evaluator = LangChainStringEvaluator(\n",
    "    \"labeled_criteria\",\n",
    "    config={\n",
    "        \"criteria\": \"relevance\",\n",
    "        \"llm\": ChatOpenAI(temperature=0.0, model=\"gpt-4o-mini\"),\n",
    "    },\n",
    "    prepare_data=lambda run, example: {\n",
    "        \"prediction\": run.outputs[\"answer\"],\n",
    "        \"reference\": run.outputs[\"context\"],  # Context 를 전달\n",
    "        \"input\": example.inputs[\"question\"],\n",
    "    },\n",
    ")\n",
    "\n",
    "print_evaluator_prompt(relevance_evaluator)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ffbe0e61",
   "metadata": {},
   "source": [
    "평가를 진행하고, 출력된 URL 로 이동하여 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ee047abc",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langsmith.evaluation import evaluate\n",
    "\n",
    "# 데이터셋 이름 설정\n",
    "dataset_name = \"RAG_EVAL_DATASET\"\n",
    "\n",
    "# 평가 실행\n",
    "experiment_results = evaluate(\n",
    "    context_answer_rag_answer,\n",
    "    data=dataset_name,\n",
    "    evaluators=[labeled_criteria_evaluator, relevance_evaluator],\n",
    "    experiment_prefix=\"LABELED-EVAL\",\n",
    "    # 실험 메타데이터 지정\n",
    "    metadata={\n",
    "        \"variant\": \"labeled_criteria evaluator 활용한 평가\",\n",
    "    },\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4a657acc",
   "metadata": {},
   "source": [
    "![](./assets/output-04.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6f44c6f",
   "metadata": {},
   "source": [
    "## 사용자 정의 점수 Evaluator(labeled_score_string)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "663e2129",
   "metadata": {},
   "source": [
    "아래는 점수를 반환하는 평가자 생성 예시입니다. `normalize_by` 를 통해 점수를 정규화할 수 있습니다. 변환된 점수는 (0 ~ 1) 사이의 값으로 정규화됩니다.\n",
    "\n",
    "아래의 `accuracy` 는 사용자가 임의로 정의한 기준입니다. 적합한 Prompt 를 정의하여 사용할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b193f4ff",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langsmith.evaluation import LangChainStringEvaluator\n",
    "\n",
    "# 점수를 반환하는 평가자 생성\n",
    "labeled_score_evaluator = LangChainStringEvaluator(\n",
    "    \"labeled_score_string\",\n",
    "    config={\n",
    "        \"criteria\": {\n",
    "            \"accuracy\": \"How accurate is this prediction compared to the reference on a scale of 1-10?\"\n",
    "        },\n",
    "        \"normalize_by\": 10,\n",
    "        \"llm\": ChatOpenAI(temperature=0.0, model=\"gpt-4o-mini\"),\n",
    "    },\n",
    "    prepare_data=lambda run, example: {\n",
    "        \"prediction\": run.outputs[\"answer\"],\n",
    "        \"reference\": example.outputs[\"answer\"],\n",
    "        \"input\": example.inputs[\"question\"],\n",
    "    },\n",
    ")\n",
    "\n",
    "print_evaluator_prompt(labeled_score_evaluator)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6644a6cf",
   "metadata": {},
   "source": [
    "평가를 진행하고, 출력된 URL 로 이동하여 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5671567e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langsmith.evaluation import evaluate\n",
    "\n",
    "# 평가 실행\n",
    "experiment_results = evaluate(\n",
    "    ask_question,\n",
    "    data=dataset_name,\n",
    "    evaluators=[labeled_score_evaluator],\n",
    "    experiment_prefix=\"LABELED-SCORE-EVAL\",\n",
    "    # 실험 메타데이터 지정\n",
    "    metadata={\n",
    "        \"variant\": \"labeled_score 활용한 평가\",\n",
    "    },\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9781c490",
   "metadata": {},
   "source": [
    "![](./assets/output-05.png)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
